<?php

/**
 * Description of weixin
 * 微信平台企业号回复被动消息
 * @author xlp
 */
include_once "sha1.php";
include_once "xmlparse.php";
include_once "pkcs7Encoder.php";

class qyweixinApi {

    private $token = null; //通讯录->回调模式中查看
    public $encodingAesKey; //通讯录->回调模式中查看
    public $corpId;   //企业CorpID，可在企业号后台设置中查看
    public $data = null; //接受的数据信息
    public $savePics = false; //是否保存推送图片到本地,true||false
    public $picPath = '/user/'; //图片保存路径
    public $voicePath = '/voice/'; //语音保存路径
    public $debug = false; //是否调试模式
    public $logPath = './log/'; //日志文件保存路径
    public $appsecret; //开发者KEY
    public $errCode = 0;
    public $errMsg = null;
    private $access_token;
    public $postXml = null;

    //请求api前缀

    const API_URL_PREFIX = 'https://api.weixin.qq.com/cgi-bin';
    //获取 access_token
    const AUTH_URL = '/token?grant_type=client_credential&';
    //获取用户基本信息
    const USER_INFO_URL = '/user/info?';
    //获取媒体数据
    const MEDIA_URL = 'http://file.api.weixin.qq.com/cgi-bin/media/get?';

    private $accessTokenPre = 'token/weixin/access_token_';

    function __construct($city_id, $debug = false, $savePics = false) {
        $setting = F('steward/setting_city_' . $city_id);
        if (!isset($setting['corp_info'])) {
            showError('缓存有误，请刷新');
        }
        $setting = $setting['corp_info'];
        if (isHave($setting['token']) && isHave($setting['encodingAESKey']) && isHave($setting['corpID'])) {
            $this->token = $setting['token'];
            $this->encodingAesKey = $setting['encodingAESKey'];
            $this->corpId = $setting['corpID'];
        } else {
            showError('api Error:请设置Token、EncodingAESKey和corpId');
        }
        $this->debug = $debug;
        $this->savePics = $savePics;
        $this->valid();
        $this->getMsg();
    }

    /**
     * 返回微信接口数据
     * @param 无
     * @return xml
     */
    public function getXml() {
        return $this->postXml;
    }

    /**
     * 回应微信接口
     * @param $data string or array 普通用户的标识，对当前公众号唯一
     * @param $code string 当前用户身份识别加密串
     * @return string 处理好的数据串
     */
    public function reply($data = '', $code = '') {
        if (!$data && $this->debug) {
            $this->saveDebugLog('内容为空', 'reply');
//            return false;
        }
        $paraEeter = array();
        if (!is_array($data)) {
            $paraEeter['Content'] = $data;
        } else {
            $paraEeter = $data;
        }
        if (!isset($paraEeter['type']) || !$paraEeter['type']) {
            $paraEeter['type'] = 'text';
        }
        if (!isset($paraEeter['createTime']) || !$paraEeter['createTime']) {
            $paraEeter['createTime'] = TIME;
        }
        if (!isset($paraEeter['flag']) || $paraEeter['flag'] != 1) {
            $paraEeter['flag'] = 0;
        }
        switch ($paraEeter['type']) {
            case 'music':
                $Content = '<Music>
			<Title><![CDATA[' . $paraEeter['title'] . ']]></Title>
			<Description><![CDATA[' . $paraEeter['description'] . ']]></Description>
			<MusicUrl><![CDATA[' . $paraEeter['musicurl'] . ']]></MusicUrl>
			<HQMusicUrl><![CDATA[' . $paraEeter['hqmusicurl'] . ']]></HQMusicUrl>
			</Music>';
                break;
            case 'news':
                $itemsCount = count($paraEeter['items']);
                $itemsCount = $itemsCount < 10 ? $itemsCount : 10;
                $newTplHeader = '<ArticleCount>' . $itemsCount . '</ArticleCount>
                        <Articles>';
                $newTplItem = "<item>
			<Title><![CDATA[%s]]></Title>
			<Description><![CDATA[%s]]></Description>
			<PicUrl><![CDATA[%s]]></PicUrl>
			<Url><![CDATA[%s]]></Url>
			</item>";
                $newTplFoot = "</Articles>";
                $Content = '';
                if ($itemsCount) {
                    foreach ($paraEeter['items'] as $key => $item) {
                        if ($key <= 9) {
//                            if (strExists($item['url'], '{#code}')) {
//                                $item['url'] = str_replace('{#code}', $code, $item['url']);
//                            }
                            $Content .= sprintf($newTplItem, $item['title'], $item['description'], $item['picurl'], $item['url']);
                        }
                    }
                }
                $Content = $newTplHeader . $Content . $newTplFoot;
                break;
            case 'service':
                $Content = '';
                $paraEeter['type'] = 'transfer_customer_service';
                break;
            default:
                $paraEeter['type'] = 'text';
//                if (strExists($paraEeter['Content'], '{#code}')) {
//                    $paraEeter['Content'] = str_replace('{#code}', $code, $paraEeter['Content']);
//                }
                $Content = '<Content><![CDATA[' . $paraEeter['Content'] . ']]></Content>';
        }
        $Content = '<xml>
    <ToUserName><![CDATA[' . $this->data['FromUserName'] . ']]></ToUserName>
    <FromUserName><![CDATA[' . $this->data['ToUserName'] . ']]></FromUserName>
    <CreateTime>' . $paraEeter['createTime'] . '</CreateTime>
    <MsgType><![CDATA[' . $paraEeter['type'] . ']]></MsgType>
    ' . $Content . '
    <FuncFlag>' . $paraEeter['flag'] . '</FuncFlag>
</xml>';
        if ($this->debug) {
            $this->saveDebugLog($Content, 'reply');
        }
        //return $Content;

        $sEncryptMsg = ""; //xml格式的密文
        $errCode = $this->EncryptMsg($Content, $_GET["timestamp"], $_GET["nonce"], $sEncryptMsg);
        if ($errCode == 0) {
            return $sEncryptMsg;
        }
    }

    /**
     * 获取用户基本信息
     * @param $openid string 普通用户的标识，对当前公众号唯一
     */
    public function getUserInfo($openid) {
        if (!$this->access_token && !$this->checkAuth()) {
            return false;
        }
        $result = getHttp(self::API_URL_PREFIX . self::USER_INFO_URL . 'access_token=' . $this->access_token . '&openid=' . $openid);
        if ($result) {
            $jsonArr = json_decode($result, true);
            if (!$jsonArr || (isset($jsonArr['errcode']) && $jsonArr['errcode'] > 0)) {
                $this->errCode = isset($jsonArr['errcode']) ? $jsonArr['errcode'] : 0;
                $this->errMsg = isset($jsonArr['errmsg']) ? $jsonArr['errmsg'] : 'error';
                if ($this->checkAccessToken()) {
                    return $this->getUserInfo($openid);
                }
                $this->saveDebugLog('Userinfo Error:openid:' . $openid . "\n" . var_export($jsonArr, true), 'getuserinfo_error');
            } else {
                return $jsonArr;
            }
        }
        return false;
    }

    /**
     * 设置APP信息
     * @param $aid int AID
     * @param $appid string 微信APPID
     * @param $appsecret string 微信 APPSecret
     */
    public function setAppInfo($aid, $appid, $appsecret) {
        $this->aid = $aid;
        $this->corpId = $appid;
        $this->appsecret = $appsecret;
    }

    /**
     * 获取微信接口信息
     * @param 无
     */
    private function getMsg() {
        //$this->postXml = isset($GLOBALS['HTTP_RAW_POST_DATA']) ? $GLOBALS['HTTP_RAW_POST_DATA'] : ''; //===TODO:注意高版本的php不支持HTTP_RAW_POST_DATA
        if (!empty($this->postXml)) {
            $this->data = (array) simplexml_load_string($this->postXml, 'SimpleXMLElement', LIBXML_NOCDATA);
            $this->data['MsgType'] = strtolower($this->data['MsgType']);
            $this->data['Content'] = isset($this->data['Content']) ? trim($this->data['Content']) : '';
            if ($this->debug) {
                $this->saveDebugLog($this->data, 'getdata');
            }
            //保存图片
            if ($this->data['MsgType'] === 'image' && $this->savePics) {
                $this->data['PicUrl'] = $this->saveImgByUrl($this->data['PicUrl']);
            }
        } else {
            $this->showRequestError(date('c') . '接受数据为空 ip:' . $_SERVER['REMOTE_ADDR'] . "\n");
        }
    }

    /* 用媒体ID保存媒体信息
     * @param $mediaID string 媒体ID
     * @param $format string 文件格式
     * @return string 本地文件相对路径地址
     */

    public function saveVoice($mediaID, $format) {
        if (!$this->access_token && !$this->checkAuth()) {
            return false;
        }
//        $this->saveDebugLog('保存语音:' . $mediaID, 'voice');
        $result = getHttp(self::MEDIA_URL . 'access_token=' . $this->access_token . '&media_id=' . $mediaID);
//        $this->saveDebugLog('语音返回:' . var_export($result, true), 'voice');
        $filename = rtrim($this->voicePath, '/') . '/' . date('Ym/d') . '/' . getRandStr(12) . '.' . $format;
        saveFile($filename, $result);
        return $filename;
    }

    /* 根据图片地址保存图片到本地
     * @param $imgUrl string 远程图片地址
     * @param $uid int UID，当存在值的时候，图片保存为用户头像
     * @return string 本地图片相对路径地址
     */

    public function saveImgByUrl($imgUrl = '', $uid = 0) {
        $content = getHttp($imgUrl, array(), true);
        if ($content) {
            if ($uid) {//保存为用户头像
                return setAvatar($uid, $content);
            } else {
                $filename = rtrim($this->picPath, '/') . '/' . date('Ym/d') . '/' . getRandStr(12) . '.jpg';
                $fileInfo = saveFile($filename, $content);
                $this->data['PicSize'] = $fileInfo['size'];
                return $fileInfo['url'];
            }
        }
        return false;
    }

    /* 保存微信日志
     * @param $uid int UID
     * @param $aid int AID
     * @return int
     */

    public function saveAccessLog($uid, $aid = 0) {
        $objData = $this->data;
        $objData['uid'] = $uid;
        $this->aid = $aid;
        $objData['aid'] = $this->aid;
        $actionType = 'new_msg'; //默认为新发消息
        $eventType = $objData['MsgType']; //事件类型
        if ($objData['MsgType'] == 'image') {//图片消息
            $objData['Content'] = $objData['PicUrl'];
        } elseif ($objData['MsgType'] == 'voice') {//语音消息
            $objData['Content'] = isset($objData['Recognition']) ? $objData['Recognition'] : (isset($objData['VoiceUrl']) ? $objData['VoiceUrl'] : $objData['MediaId']);
        } elseif ($objData['MsgType'] == 'video') {//视频消息
            $objData['Content'] = $objData['ThumbMediaId'] . '|' . $objData['MediaId'];
        } elseif ($objData['MsgType'] == 'location') {//地理位置消息
            $objData['Content'] = $objData['Location_X'] . '|' . $objData['Location_Y'] . '|' . $objData['Scale'] . '|' . $objData['Label'];
        } elseif ($objData['MsgType'] == 'link') {//链接消息
            $objData['Content'] = $objData['Title'] . '|' . $objData['Description'] . '|' . $objData['Url'];
        } elseif ($objData['MsgType'] == 'event') {
            switch ($objData['Event']) {
                case 'subscribe'://新增用户
                    $actionType = 'new_user';
                    $objData['Content'] = $objData['Event'];
                    break;
                case 'unsubscribe'://取消关注
                    $actionType = 'new_cancel';
                    $objData['Content'] = $objData['Event'];
                    break;
                default://点击菜单
                    $objData['Content'] = $objData['EventKey'];
            }
            $eventType = $objData['Event'];
        }
        if (!isHave($objData['MsgId'])) {
            $objData['MsgId'] = 0;
        }
        if ($objData['Content']) {
            $objData['Content'] = htmlspecialchars($objData['Content']);
        }
        $objData['msg_type_id'] = $this->getEventId($eventType); //获取消息类型
        $this->saveAccessStat($actionType);
        $list = array('aid' => 'aid', 'uid' => 'uid', 'msg_id' => 'MsgId', 'msg_type' => 'msg_type_id', 'content' => 'Content', 'create_time' => 'CreateTime');
        $data = array();
        foreach ($list as $key => $val) {
            $data[$key] = isset($objData[$val]) ? $objData[$val] : '';
        }
        //======根据msgid进行信息排重===============
        //存在MsgId就直接用MsgId进行处理
        //推送事件不包含MsgId，则用FromUserName + CreateTime进行处理，此处用uid+CreateTime来进行处理
        //======根据msgid进行信息排重===============
        return M('access_log')->insert($data);
    }

    /**
     * 删除验证数据
     * @param string $appid
     */
    public function resetAuth() {
        $this->access_token = '';
        S($this->accessTokenPre . $this->corpId, null);
        $this->checkAuth();
        return true;
    }

    /*
     * 检查是否需要重新获取accesstoken
     * 并记下日志
     */

    public function checkAccessToken() {
        if ($this->errCode == 40001) {
            $this->resetAuth();
            saveLog('accessToken/reset', 'weixin');
            return true;
        }
        return false;
    }

    /**
     * 获取和验证access_token
     */
    private function checkAuth() {
        if (!$this->corpId || !$this->appsecret) {
            saveLog('accessToken/lose', 'weixin');
            return false;
        }
        if ($this->access_token) {
            return $this->access_token;
        }
        $this->access_token = S($this->accessTokenPre . $this->corpId);
        if ($this->access_token) {
            return $this->access_token;
        }
        $result = getHttp(self::API_URL_PREFIX . self::AUTH_URL . 'appid=' . $this->corpId . '&secret=' . $this->appsecret);
        if ($result) {
            $jsonArr = json_decode($result, true);
            if (!$jsonArr || (isset($jsonArr['errcode']) && $jsonArr['errcode'] > 0)) {
                $this->saveDebugLog('Auth Error:' . var_export($jsonArr, true), 'error');
            } else {
                $this->access_token = $jsonArr['access_token'];
                // 将access_token保存到缓存中
                S($this->accessTokenPre . $this->corpId, $jsonArr['access_token'], $jsonArr['expires_in'] - 100);
            }
        }
        return $this->access_token;
    }

    /* 记录访问统计
     * @param $field string 记录类型 1:新增用户，2:接受消息
     */

    private function getEventId($type) {
        switch (strtolower($type)) {
            //用户上行文本信息
            case 'text';
                $objId = 2;
                break;
            //用户上行位置信息
            case 'location';
                $objId = 3;
                break;
            //用户上行图片消息
            case 'image';
                $objId = 4;
                break;
            //用户发送语音信息
            case 'voice';
                $objId = 5;
                break;
            //用户上行连接信息
            case 'link';
                $objId = 6;
                break;
            //用户上行视频信息
            case 'video';
                $objId = 7;
                break;
            //用户关注
            case 'subscribe';
                $objId = 8;
                break;
            //用户上行视频信息
            case 'unsubscribe';
                $objId = 9;
                break;
            //扫描二维码
            case 'scan':
                $objId = 10;
                break;
            //event事件
            default:
                $objId = 1;
        }
        return $objId;
    }

    /* 记录访问统计
     * @param $field string 记录类型 1:新增用户，2:接受消息
     */

    private function saveAccessStat($field = 'new_msg') {
        $where = array('aid' => $this->aid, 'infotime' => mktime(0, 0, 0, date('m'), date('d'), date('Y')));
        if (M('access_stat')->field('id')->where($where)->find()) {//存在则更新
            return M('access_stat')->where($where)->setInc($field, 1);
        } else {//不存在则添加
            $where['aid'] = $this->aid;
            $where['new_user'] = 0;
            $where['new_msg'] = 0;
            $where['new_cancel'] = 0;
            $where[$field] = 1;
            return M('access_stat')->insert($where);
        }
    }

    /* 记录测试日志文件  
     */

    private function saveDebugLog($data = '', $logType = 'request') {
        saveLog('weixin/' . $logType, $data);
    }

    /* 非法访问处理
     */

    private function showRequestError($data) {
        $this->saveDebugLog($data, 'error');
        header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
        exit;
    }

    /* 验证地址
     */

    private function valid() {
        if ($_SERVER['REQUEST_METHOD'] == 'GET') {
            $sEchoStr = isset($_GET['echostr']) ? $_GET['echostr'] : '';
            if ($sEchoStr) {
                $sMsgSignature = isset($_GET["msg_signature"]) ? $_GET["msg_signature"] : '';
                $sTimeStamp = isset($_GET['timestamp']) ? $_GET['timestamp'] : '';
                $sNonce = isset($_GET['nonce']) ? $_GET['nonce'] : '';
                if (strlen($this->encodingAesKey) != 43) {
                    return false;
                }

                //verify msg_signature
                $sha1 = new SHA1;
                $array = $sha1->getSHA1($this->token, $sTimeStamp, $sNonce, $sEchoStr);
                $ret = $array[0];
                if ($ret != 0) {
                    return false;
                }

                $signature = $array[1];
                if ($signature != $sMsgSignature) {
                    return false;
                }


                $pc = new Prpcrypt($this->encodingAesKey);
                $result = $pc->decrypt($sEchoStr, $this->corpId);
                if ($result[0] != 0) {
                    return false;
                }
                echo $result[1];
                exit();
            }
            return true;
        } else {
            if (!$this->checkSignature()) {
                $this->showRequestError(date('c') . '非法数据请求 ip:' . $_SERVER['REMOTE_ADDR'] . "\n");
            }
        }
    }

    /**
     * 检验消息的真实性，并且获取解密后的明文.
     * <ol>
     *    <li>利用收到的密文生成安全签名，进行签名验证</li>
     *    <li>若验证通过，则提取xml中的加密消息</li>
     *    <li>对消息进行解密</li>
     * </ol>
     *
     * @param $msgSignature string 签名串，对应URL参数的msg_signature
     * @param $timestamp string 时间戳 对应URL参数的timestamp
     * @param $nonce string 随机串，对应URL参数的nonce
     * @param $postData string 密文，对应POST请求的数据
     * @param &$msg string 解密后的原文，当return返回0时有效
     *
     * @return int 成功0，失败返回对应的错误码
     */
    private function checkSignature() {
        if ($this->debug) {
            return true;
        }
        $sMsgSignature = isset($_GET["msg_signature"]) ? $_GET["msg_signature"] : '';
        $sTimeStamp = isset($_GET['timestamp']) ? $_GET['timestamp'] : '';
        $sNonce = isset($_GET['nonce']) ? $_GET['nonce'] : '';

        if (strlen($this->encodingAesKey) != 43) {
            return false;
        }

        //提取密文
        $postDate = isset($GLOBALS['HTTP_RAW_POST_DATA']) ? $GLOBALS['HTTP_RAW_POST_DATA'] : ''; //===TODO:注意高版本的php不支持HTTP_RAW_POST_DATA
        //提取密文
        $xmlparse = new XMLParse;
        $array = $xmlparse->extract($postDate);
        $ret = $array[0];

        if ($ret != 0) {
            return false;
        }
        if ($sTimeStamp == null) {
            $sTimeStamp = time();
        }
        $encrypt = $array[1];
        $touser_name = $array[2];
        //验证安全签名
        $sha1 = new SHA1;
        $array = $sha1->getSHA1($this->token, $sTimeStamp, $sNonce, $encrypt);
        $ret = $array[0];

        if ($ret != 0) {
            return false;
        }
        $signature = $array[1];

        if ($signature != $sMsgSignature) {
            return false;
        }
        $pc = new Prpcrypt($this->encodingAesKey);
        $result = $pc->decrypt($encrypt, $this->corpId);
        if ($result[0] != 0) {
            return false;
        }
        $this->postXml = $result[1];
        return true;
    }

    /**
     * 将公众平台回复用户的消息加密打包.
     * <ol>
     *    <li>对要发送的消息进行AES-CBC加密</li>
     *    <li>生成安全签名</li>
     *    <li>将消息密文和安全签名打包成xml格式</li>
     * </ol>
     *
     * @param $replyMsg string 公众平台待回复用户的消息，xml格式的字符串
     * @param $timeStamp string 时间戳，可以自己生成，也可以用URL参数的timestamp
     * @param $nonce string 随机串，可以自己生成，也可以用URL参数的nonce
     * @param &$encryptMsg string 加密后的可以直接回复用户的密文，包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
     *                      当return返回0时有效
     *
     * @return int 成功0，失败返回对应的错误码
     */
    public function EncryptMsg($sReplyMsg, $sTimeStamp, $sNonce, &$sEncryptMsg) {
        $pc = new Prpcrypt($this->encodingAesKey);

        //加密
        $array = $pc->encrypt($sReplyMsg, $this->corpId);
        $ret = $array[0];
        if ($ret != 0) {
            return false;
        }

        if ($sTimeStamp == null) {
            $sTimeStamp = time();
        }
        $encrypt = $array[1];

        //生成安全签名
        $sha1 = new SHA1;
        $array = $sha1->getSHA1($this->token, $sTimeStamp, $sNonce, $encrypt);
        $ret = $array[0];
        if ($ret != 0) {
            return false;
        }
        $signature = $array[1];

        //生成发送的xml
        $xmlparse = new XMLParse;
        $sEncryptMsg = $xmlparse->generate($encrypt, $signature, $sTimeStamp, $sNonce);
        return ErrorCode::$OK;
    }

}
